package org.snail.common;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;

/**
 * spi类加载器
 * <p>
 * Created by fish on 17/4/8.
 */
public class SnailClassLoader<I> implements Iterable<I> {

    /**
     * 路径
     */
    private static final String PREFIX = "META-INF/services/";

    private final Class<I> service;

    private final ClassLoader loader;

    /**
     * 简单实现cache
     */
    private LinkedHashMap<String, I> providers = new LinkedHashMap<>();

    private LazyIterator lookupIterator;

    public static <I> SnailClassLoader<I> load(Class<I> service) {
        return SnailClassLoader.load(service, Thread.currentThread().getContextClassLoader());
    }

    public static <I> SnailClassLoader<I> load(Class<I> service, ClassLoader loader) {
        return new SnailClassLoader<>(service, loader);
    }

    public I first() {
        return iterator().next();
    }

    public I find(String implName) {
        for (I i : providers.values()) {
            Spi spi = i.getClass().getAnnotation(Spi.class);
            if (spi != null && spi.name().equalsIgnoreCase(implName)) {
                return i;
            }
        }
        while (lookupIterator.hasNext()) {
            KeyValue<String, Class<I>> e = lookupIterator.next();
            String name = e.getKey();
            Class<I> cls = e.getValue();
            Spi spi = cls.getAnnotation(Spi.class);
            if (spi != null && spi.name().equalsIgnoreCase(implName)) {
                try {
                    I provider = service.cast(cls.newInstance());
                    providers.put(name, provider);
                    return provider;
                } catch (Throwable x) {
                    throw fail(service, "provider " + name + " could not be instantiated", x);
                }
            }
        }
        throw fail(service, "provider " + implName + " could not be found");
    }

    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

    private SnailClassLoader(Class<I> service, ClassLoader loader) {
        if (service == null) {
            throw new NullPointerException("service interface cannot be null");
        }
        this.service = service;
        this.loader = (loader == null) ? ClassLoader.getSystemClassLoader() : loader;
        reload();
    }

    private static ServiceConfigurationError fail(Class<?> service, String msg, Throwable cause) {
        return new ServiceConfigurationError(service.getName() + ": " + msg, cause);
    }

    private static ServiceConfigurationError fail(Class<?> service, String msg) {
        return new ServiceConfigurationError(service.getName() + ": " + msg);
    }

    private static ServiceConfigurationError fail(Class<?> service, URL url, int line, String msg) {
        return fail(service, url + ":" + line + ": " + msg);
    }


    /**
     * 读取文件,解析路径。
     *
     * @param service
     * @param url
     * @param reader
     * @param line
     * @param names
     */
    private int parseLine(Class<?> service, URL url, BufferedReader reader, int line, List<String> names)
            throws IOException, ServiceConfigurationError {

        String readLine = reader.readLine();
        if (readLine == null) {
            return -1;
        }
        int ci = readLine.indexOf('#');
        if (ci >= 0) {
            readLine = readLine.substring(0, ci);
        }
        readLine = readLine.trim();
        int n = readLine.length();
        if (n != 0) {
            if ((readLine.indexOf(' ') >= 0) || (readLine.indexOf('\t') >= 0)) {
                throw fail(service, url, line, "illegal configuration-file syntax");
            }
            int cp = readLine.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp)) {
                throw fail(service, url, line, "illegal provider-class name: " + readLine);
            }
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = readLine.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) {
                    throw fail(service, url, line, "Illegal provider-class name: " + readLine);
                }
            }
            if (!providers.containsKey(readLine) && !names.contains(readLine)) {
                names.add(readLine);
            }
        }
        return line + 1;
    }

    @SuppressWarnings("all")
    private Iterator<String> parse(Class<?> service, URL url) {
        InputStream in = null;
        BufferedReader reader = null;
        List<String> names = new ArrayList<>();
        try {
            in = url.openStream();
            reader = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int line = 1;
            while ((line = parseLine(service, url, reader, line, names)) >= 0) ;
        } catch (IOException x) {
            throw fail(service, "error reading configuration file", x);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException y) {
                throw fail(service, "error closing configuration file", y);
            }
        }
        return names.iterator();
    }

    @Override
    public Iterator<I> iterator() {
        return new Iterator<I>() {

            Iterator<Map.Entry<String, I>> knownProviders = providers.entrySet().iterator();

            @Override
            public boolean hasNext() {
                return knownProviders.hasNext() || lookupIterator.hasNext();
            }

            @Override
            public I next() {
                if (knownProviders.hasNext()) {
                    return knownProviders.next().getValue();
                }
                KeyValue<String, Class<I>> KeyValue = lookupIterator.next();
                String name = KeyValue.getKey();
                Class<I> cls = KeyValue.getValue();
                try {
                    I provider = service.cast(cls.newInstance());
                    providers.put(name, provider);
                    return provider;
                } catch (Throwable x) {
                    throw fail(service, "provider(" + name + " )could not be instantiated", x);
                }
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    private class LazyIterator implements Iterator<KeyValue<String, Class<I>>> {
        Class<I> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<I> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        @Override
        public boolean hasNext() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null) {
                        configs = ClassLoader.getSystemResources(fullName);
                    } else {
                        configs = loader.getResources(fullName);
                    }
                } catch (IOException x) {
                    throw fail(service, "error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        @Override
        public KeyValue<String, Class<I>> next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            String name = nextName;
            nextName = null;
            Class<?> cls;
            try {
                cls = Class.forName(name, false, loader);
            } catch (ClassNotFoundException x) {
                throw fail(service, "provider " + name + " not found");
            }
            if (!service.isAssignableFrom(cls)) {
                throw fail(service, "provider " + name + " not a subtype");
            }
            return KeyValue.of(name, (Class<I>) cls);
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }


}
